/*
 * Freshtel FT-102 VoIP USB Phone driver
 *
 * Copyright (c) 2007 Sebastien Bourdeauducq <sebastien.bourdeauducq@gmail.com>
 * Copyright (c) 2005, 2006 Henk Vergonet <Henk.Vergonet@gmail.com>
 *
 * This program is free software; you can redistribute it and/or
 * modify it under the terms of the GNU General Public License as
 * published by the Free Software Foundation; either version 2 of
 * the License, or (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program; if not, write to the Free Software
 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
 */

#include <linux/kernel.h>
#include <linux/module.h>
#include <linux/input.h>
#include <linux/cdev.h>
#include <linux/rwsem.h>
#include <linux/mm.h>
#include <linux/vmalloc.h>
#include <linux/ptrace.h>
#include <linux/slab.h>
#include <linux/usb.h>
#include <linux/usb/input.h>

#include <asm/uaccess.h>
#include <asm/io.h>

#include "freshtel.h"
#include "freshtel_ioctl.h"

#define DRIVER_VERSION "ftd-1.0"
#define DRIVER_AUTHOR "Sebastien Bourdeauducq"
#define DRIVER_DESC "Freshtel FT-102 VoIP USB Phone driver"

#define usb_free_coherent usb_buffer_free
#define usb_alloc_coherent usb_buffer_alloc

struct freshtel_dev {
	struct input_dev *idev;		/* input device */
	struct usb_device *udev;	/* usb device */

	/* irq input channel */
	struct ftd_packet_short	*irq_data;
	dma_addr_t		irq_dma;
	struct urb		*urb_irq;

	char phys[64]; /* physical device path */

	int pressed_key;
	
	unsigned char *lcd_canonical;
	unsigned char lcd_wanted[LCD_BUFSIZE];
	unsigned char lcd_displayed[LCD_BUFSIZE];
	
	struct cdev devnode;
	struct semaphore sem;
};

static int map_scancode_to_key(unsigned scancode);

/*******************************************************************************
 * USB communication
 ******************************************************************************/

static int freshtel_cmd(struct freshtel_dev *ftd, struct ftd_packet_long *p)
{
	unsigned char *buf = (unsigned char *)p;
	int i;
	
	dbg("sending cmd %x", p->cmd);

	p->sum = 0;
	for(i=0;i<USB_PKT_LEN-1;i++) p->sum -= buf[i];
	return usb_control_msg(ftd->udev,
			usb_sndctrlpipe(ftd->udev, 0),
			USB_REQ_SET_CONFIGURATION,
			USB_TYPE_CLASS | USB_RECIP_INTERFACE | USB_DIR_OUT,
			0x200, 3,
			p, sizeof(*p),
			USB_CTRL_SET_TIMEOUT);
}

static void urb_irq_callback(struct urb *urb)
{
	struct freshtel_dev *ftd = urb->context;
	int ret;
	
	if(urb->status)
		err("%s - urb status %d", __FUNCTION__, urb->status);

	switch(ftd->irq_data->cmd) {
		case CMD_INIT:
			/* nothing to do */
			break;
		case CMD_SCANCODE: {
			unsigned char scancode;
			int key;
			
			scancode = ftd->irq_data->data[0];
			
			dbg("get scancode %x", scancode);
		
			/* Scancode 0xff means the pressed key has been released */
			if(scancode == 0xff) {
				if(ftd->pressed_key == -1) {
					err("received key release event with no key pressed");
				} else {
					/* key release */
					input_report_key(ftd->idev, ftd->pressed_key & 0xff, 0);
					if(ftd->pressed_key >> 8)
						input_report_key(ftd->idev, ftd->pressed_key >> 8, 0);
					ftd->pressed_key = -1;
				}
			} else {
				key = map_scancode_to_key(scancode);

				if(key > 0) {
					/* key press */
					input_report_key(ftd->idev, key & 0xff, 1);
					if(key >> 8)
						input_report_key(ftd->idev, key >> 8, 1);
					ftd->pressed_key = key;
				} else err("unknown scancode 0x%x", scancode);
			}
			
			input_sync(ftd->idev);
			
			break;
		}

		default:
			err("unexpected response %x", ftd->irq_data->cmd);
			break;
	}
	
	ret = usb_submit_urb(ftd->urb_irq, GFP_ATOMIC);
}

static int freshtel_init(struct freshtel_dev *ftd)
{
	struct ftd_packet_long p;
	int ret;

	memset(&p, 0, sizeof(p));
	p.cmd = CMD_INIT;
	p.size = 0x0b;
	if((ret = freshtel_cmd(ftd, &p)) < 0) return ret;
		
	return 0;
}

/*******************************************************************************
 * LCD
 ******************************************************************************/

/* The LCD offsets are a complete mess-up. The screen is divided in 44x8 chunks, 
   except the last one which is 8x8, starting from the bottom right corner.
   This table contains the offsets of the start of each of these chunks, which are
   quite random. */

/* "C'est pas tres dur, et fort casse-pieds." (c) Gerard D. */
static const int freshtel_chunk_offsets[LCD_CHUNKCOUNT] = {
	0x0000,
	0x002c,
	0x0058,
	0x011c,
	0x0148,
	0x020c,
	0x0238,
	0x0264,
	0x0328,
	0x0354,
	0x0418,
	0x0444,
	0x0508,
	0x0534,
	0x0560,
};

static int freshtel_lcd_update(struct freshtel_dev *ftd)
{
	struct ftd_packet_long p;
	int i;
	int chunksize;
	int chunkoffset_driver;
	int chunkoffset_device;
	int j, damaged;
	int ret;
	
	p.cmd = CMD_LCD;
	
	/* Find and update damaged chunks */
	for(i=0;i<LCD_CHUNKCOUNT;i++) {
		chunksize = (i == (LCD_CHUNKCOUNT - 1)) ? LCD_LASTCHUNKSIZE : LCD_CHUNKSIZE;
		chunkoffset_driver = i*LCD_CHUNKSIZE;
		
		damaged = 0;
		for(j=0;j<chunksize;j++)
			if(ftd->lcd_wanted[chunkoffset_driver+j] != ftd->lcd_displayed[chunkoffset_driver+j]) {
				damaged = 1;
				break;
			}
		if(damaged) {
			chunkoffset_device = freshtel_chunk_offsets[i];
			p.size = chunksize;
			p.offset = cpu_to_le16(chunkoffset_device);
			memcpy(p.data, &ftd->lcd_wanted[chunkoffset_driver], chunksize);
			memcpy(&ftd->lcd_displayed[chunkoffset_driver], &ftd->lcd_wanted[chunkoffset_driver], chunksize);
			if((ret = freshtel_cmd(ftd, &p)) < 0) return ret;
			/* Wait for the device to update its display */
			msleep_interruptible(10);
		}
	}
	
	return 0;
}

/* Must call freshtel_lcd_update after */
/* 0 <= x < LCD_W, 0 <= y < LCD_H */
static void lcd_set_pixel(struct freshtel_dev *ftd, int x, int y)
{
	int line;
	int bit;
	int offset;
	
	x = LCD_W - x - 1;
	y = LCD_H - y - 1;
	
	line = y / 8;
	bit = y % 8;
	offset = line*LCD_W+x;
	
	ftd->lcd_wanted[offset] |= (1 << bit);
}

/* Convert from canonical pixel format to Freshtel pixel format */
static void lcd_convert(struct freshtel_dev *ftd)
{
	int x, y;
	
	memset(&ftd->lcd_wanted[0], 0, LCD_BUFSIZE);
	for(y=0;y<LCD_H;y++)
		for(x=0;x<LCD_W;x++)
			if(ftd->lcd_canonical[(y*LCD_W+x)/8] & (0x80 >> (x % 8)))
				lcd_set_pixel(ftd, x, y);
}

static int freshtel_backlight_enable(struct freshtel_dev *ftd, int enable)
{
	struct ftd_packet_long p;
	int ret;
	
	memset(&p, 0, sizeof(p));
	p.cmd = CMD_LCD_BACKLIGHT;
	p.size = 1;
	p.data[0] = enable ? 1 : 0;
	if((ret = freshtel_cmd(ftd, &p)) < 0) return ret;
		
	return 0;
}

/*******************************************************************************
 * Keyboard
 ******************************************************************************/

static int map_scancode_to_key(unsigned scancode)
{
	static const int map[] = {		/* code	key	*/
		KEY_1,			 	/* 00 1		*/
		KEY_2,				/* 01 2		*/
		KEY_3,				/* 02 3		*/
		KEY_ENTER,			/* 03 pickup	*/
		-EINVAL,			/* 04		*/
		-EINVAL,			/* 05		*/
		-EINVAL,			/* 06		*/
		-EINVAL,			/* 07		*/
		KEY_4,				/* 10 4		*/
		KEY_5,				/* 11 5		*/
		KEY_6,				/* 12 6		*/
		KEY_ESC,			/* 13 hangup	*/
		KEY_LEFTSHIFT | KEY_C << 8,	/* 14 contacts  */
		-EINVAL,			/* 15		*/
		-EINVAL,			/* 16		*/
		-EINVAL,			/* 17		*/
		KEY_7,				/* 20 7		*/
		KEY_8,				/* 21 8		*/
		KEY_9,				/* 22 9		*/
		-EINVAL,			/* 23   	*/
		KEY_DOWN,			/* 24 down	*/
		-EINVAL,			/* 25		*/
		-EINVAL,			/* 26		*/
		-EINVAL,			/* 27		*/
		KEY_KPASTERISK,			/* 30 *		*/
		KEY_0,				/* 31 0		*/
		KEY_LEFTSHIFT | KEY_3 << 8,	/* 32 #		*/
		KEY_UP,				/* 33 up	*/
		KEY_BACKSPACE,			/* 34 C		*/
	};
	if(scancode & 0x8)
		return -EINVAL;
	scancode = (scancode & 7) | ((scancode & 0xf0) >> 1);

	if(scancode < ARRAY_SIZE(map))
		return map[scancode];

	return -EINVAL;
}


/*******************************************************************************
 * Ringtones
 ******************************************************************************/

static unsigned char default_ringtone[] = {
	0xFF,			/* volume [0-255] */
	0xFB, 0x1E, 0x00, 0x0C,	/* 1250 [hz], 12/100 [s] */
	0xFC, 0x18, 0x00, 0x0C,	/* 1000 [hz], 12/100 [s] */
	0xFB, 0x1E, 0x00, 0x0C,
	0xFC, 0x18, 0x00, 0x0C,
	0xFB, 0x1E, 0x00, 0x0C,
	0xFC, 0x18, 0x00, 0x0C,
	0xFB, 0x1E, 0x00, 0x0C,
	0xFC, 0x18, 0x00, 0x0C,
	0xFF, 0xFF, 0x01, 0x90,	/* silent, 400/100 [s] */
	0x00, 0x00		/* end of sequence */
};

static int freshtel_set_ringtone(struct freshtel_dev *ftd, unsigned char *buf, size_t size)
{
	struct ftd_packet_long p;
	int ix, len;
	int ret;

	if (size < 1)
		return -EINVAL;

	/* Set the ringtone volume */
	memset(&p, 0, sizeof(p));
	p.cmd = CMD_RING_VOLUME;
	p.size = 1;
	p.data[0] = buf[0];
	if((ret = freshtel_cmd(ftd, &p)) < 0) return ret;

	buf++;
	size--;

	p.cmd = CMD_RING_NOTE;
	ix = 0;
	while(size != ix) {
		len = size - ix;
		if(len > sizeof(p.data)) len = sizeof(p.data);
		p.size = len;
		p.offset = cpu_to_le16(ix);
		memcpy(p.data, &buf[ix], len);
		if((ret = freshtel_cmd(ftd, &p)) < 0) return ret;
		ix += len;
	}
	return 0;
}

static int freshtel_ring(struct freshtel_dev *ftd, char count)
{
	struct ftd_packet_long p;

	err("in freshtel_ring");
	p.cmd = CMD_RING;
	p.size = 1;
	p.data[0] = count;
	return freshtel_cmd(ftd, &p);
}

/*******************************************************************************
 * Input event interface
 ******************************************************************************/

static int input_open(struct input_dev *dev)
{
	struct freshtel_dev *ftd = input_get_drvdata(dev);
	int ret;

	dbg("%s", __FUNCTION__);
	
	ftd->pressed_key = -1; /* no key pressed */
	
	if((ret = usb_submit_urb(ftd->urb_irq, GFP_KERNEL)) != 0) {
		dbg("%s - usb_submit_urb failed with result %d",
		     __FUNCTION__, ret);
		return ret;
	}
	
	return 0;
}

static void input_close(struct input_dev *dev)
{
	struct freshtel_dev *ftd = input_get_drvdata(dev);

	usb_kill_urb(ftd->urb_irq);
}

/*******************************************************************************
 * Device node interface
 ******************************************************************************/

#define FRESHTEL_MAX_DEVICES 16

static dev_t freshtel_dev;

static int ftdev_fault(struct vm_area_struct *vma, struct vm_fault *vmf)
{
	struct freshtel_dev *ftd = (struct freshtel_dev *)vma->vm_private_data;
	struct page *page; 
	int faultCode = VM_FAULT_SIGBUS;
	
	down(&ftd->sem);
	if(vmf->pgoff >= LCD_BUFSIZE)
		goto out; /* out of range */

	faultCode = 0;
	page = vmalloc_to_page(ftd->lcd_canonical);
	
	/* got it, now increment the count */
	get_page(page);
	vmf->page = page;
out:
	up(&ftd->sem);
	return faultCode;
}


static struct vm_operations_struct ftdev_vm_ops = {
	.fault = ftdev_fault,
};

static int ftdev_mmap(struct file *file, struct vm_area_struct *vma)
{
	/* don't do anything here: page fault handler will fill the holes */
	vma->vm_ops = &ftdev_vm_ops;
	vma->vm_flags |= VM_RESERVED;
	vma->vm_private_data = file->private_data;

	return 0;
}

static ssize_t ftdev_read(struct file *file, char *buf, size_t count, loff_t *f_pos)
{
	struct freshtel_dev *ftd = (struct freshtel_dev *)file->private_data;
	ssize_t ret;
	
	ret = 0;
	if(down_interruptible(&ftd->sem)) return -ERESTARTSYS;
	if(*f_pos >= LCD_BUFSIZE) goto out;
	if(*f_pos + count > LCD_BUFSIZE) count = LCD_BUFSIZE - *f_pos;
 
	if(copy_to_user(buf, &ftd->lcd_canonical[*f_pos], count)) {
		ret = -EFAULT;
		goto out;
	}
	*f_pos += count;
	ret = count;

out:
	up(&ftd->sem);
	return ret;
}

static ssize_t ftdev_write(struct file *file, const char *buf, size_t count, loff_t *f_pos)
{
	struct freshtel_dev *ftd = (struct freshtel_dev *)file->private_data;
	ssize_t ret;
	
	ret = 0;
	if(down_interruptible(&ftd->sem)) return -ERESTARTSYS;
	if(*f_pos >= LCD_BUFSIZE) goto out;
	if(*f_pos + count > LCD_BUFSIZE) count = LCD_BUFSIZE - *f_pos;
 
	if(copy_from_user(&ftd->lcd_canonical[*f_pos], buf, count)) {
		ret = -EFAULT;
		goto out;
	}
	*f_pos += count;
	ret = count;
	
out:
	up(&ftd->sem);
	return ret;
}

static loff_t ftdev_llseek(struct file *file, loff_t offset, int whence)
{
	struct freshtel_dev *ftd = (struct freshtel_dev *)file->private_data;
	int ret;
	
	ret = 0;
	if(down_interruptible(&ftd->sem)) return -ERESTARTSYS;
	switch(whence) {
		case 0:
			file->f_pos = offset;
			ret = file->f_pos;
			force_successful_syscall_return();
			break;
		case 1:
			file->f_pos += offset;
			ret = file->f_pos;
			force_successful_syscall_return();
			break;
		case 2:
			file->f_pos = LCD_BUFSIZE-offset;
			ret = file->f_pos;
			force_successful_syscall_return();
			break;
		default:
			ret = -EINVAL;
			break;
	}
	up(&ftd->sem);
	return ret;
}

static int ftdev_ioctl(struct inode *inode, struct file *file, unsigned int cmd, unsigned long arg)
{
	struct freshtel_dev *ftd = (struct freshtel_dev *)file->private_data;
	int err;

	if(down_interruptible(&ftd->sem)) return -ERESTARTSYS;
		
	switch(cmd) {
		case FRESHTEL_SCREENSIZE: {
			struct freshtel_screensize ss;
				
			ss.w = LCD_W;
			ss.h = LCD_H;
			

			err("FRESHTEL_SCREENSIZE: %dx%d", LCD_W, LCD_H);
			if(copy_to_user((void *)arg, &ss, sizeof(ss)))
				err = -EFAULT;
			else
				err = 0;
			err("FRESHTEL_SCREENSIZE: ret: %i", err);
			break;
		}
		case FRESHTEL_IOCUPDATE:
			lcd_convert(ftd);
			err = freshtel_lcd_update(ftd);
			break;
		case FRESHTEL_IOCSBACKLIGHT:
			err = freshtel_backlight_enable(ftd, arg);
			break;
		case FRESHTEL_IOCRING:
			err = freshtel_ring(ftd, arg);
			break;
		case FRESHTEL_IOCSRINGTONE: {
			struct freshtel_sringtone ringtone;
				
			if(copy_from_user(&ringtone, (void *)arg, sizeof(ringtone))) {
				err = -EFAULT; 
				break;
			}
			err = freshtel_set_ringtone(ftd, ringtone.data, ringtone.size);
			break;
		}
		default:
			err = -ENOTTY;
			break;
	}
	
	up(&ftd->sem);
	
	return err;
}

static int ftdev_open(struct inode *inode, struct file *file)
{
	struct cdev *devnode = inode->i_cdev;

err("in ftdev_open\n");
	
	file->private_data = container_of(devnode, struct freshtel_dev, devnode);
	return 0;
}

static int ftdev_release(struct inode *inode, struct file *file)
{
	return 0;
}

static struct file_operations ftdev_fops = {
        .owner          = THIS_MODULE,
	.read		= ftdev_read,
	.write		= ftdev_write,
	.llseek		= ftdev_llseek,
	.mmap		= ftdev_mmap,
        .ioctl          = ftdev_ioctl,
        .open           = ftdev_open,
        .release        = ftdev_release,
};

static int register_devnode(struct freshtel_dev *ftd, int minor)
{
	dev_t devno;
	int ret;
	
	devno = MKDEV(MAJOR(freshtel_dev), MINOR(freshtel_dev) + minor);
	cdev_init(&ftd->devnode, &ftdev_fops);
	init_MUTEX(&ftd->sem);
	if((ret = cdev_add(&ftd->devnode, devno, 1))) return ret;

	return 0;
}

static int unregister_devnode(struct freshtel_dev *ftd)
{
	cdev_del(&ftd->devnode);
	return 0;
}

/*******************************************************************************
 * Linux interface and usb initialisation
 ******************************************************************************/

static const struct usb_device_id usb_table[] = {
	{
		.match_flags		= USB_DEVICE_ID_MATCH_DEVICE|USB_DEVICE_ID_MATCH_INT_INFO,
		.idVendor		= 0x6993,
		.idProduct		= 0xb001,
		.bInterfaceClass	= USB_CLASS_HID,
		.bInterfaceSubClass	= 0,
		.bInterfaceProtocol	= 0,
	},
	{ }
};

static int usb_cleanup(struct freshtel_dev *ftd, int err)
{
	if(ftd == NULL)
		return err;

	usb_kill_urb(ftd->urb_irq);	/* parameter validation in core/urb */

        if(ftd->idev) {
		if(err)
			input_free_device(ftd->idev);
		else
			input_unregister_device(ftd->idev);
	}
	if(ftd->irq_data)
		usb_free_coherent(ftd->udev, USB_PKT_LEN,
				  ftd->irq_data, ftd->irq_dma);

	usb_free_urb(ftd->urb_irq);	/* parameter validation in core/urb */
	kfree(ftd);
	return err;
}

static void usb_disconnect(struct usb_interface *intf)
{
	struct freshtel_dev *ftd;

	ftd = usb_get_intfdata(intf);
	down_interruptible(&ftd->sem);
	unregister_devnode(ftd);
	usb_set_intfdata(intf, NULL);
	up(&ftd->sem);

	usb_cleanup(ftd, 0);
	vfree(ftd->lcd_canonical);
}

static int usb_probe(struct usb_interface *intf, const struct usb_device_id *id)
{
	struct usb_device *udev = interface_to_usbdev(intf);
	struct usb_host_interface *interface;
	struct usb_endpoint_descriptor *endpoint;
	struct freshtel_dev *ftd;
	struct input_dev *input_dev;
	int ret, pipe, i;

	interface = intf->cur_altsetting;
	endpoint = &interface->endpoint[0].desc;
	if(!(endpoint->bEndpointAddress & USB_DIR_IN))
		return -EIO;
	if((endpoint->bmAttributes & USB_ENDPOINT_XFERTYPE_MASK) != USB_ENDPOINT_XFER_INT)
		return -EIO;

	ftd = kzalloc(sizeof(struct freshtel_dev), GFP_KERNEL);
	if(!ftd) return -ENOMEM;
		
	ftd->lcd_canonical = vmalloc(LCD_BUFSIZE);

	ftd->udev = udev;

	ftd->idev = input_dev = input_allocate_device();
	if(!input_dev)
		return usb_cleanup(ftd, -ENOMEM);

	/* allocate usb buffers */
	ftd->irq_data = usb_alloc_coherent(udev, USB_PKT_LEN, GFP_ATOMIC,
					   &ftd->irq_dma);
	if(ftd->irq_data == NULL) return usb_cleanup(ftd, -ENOMEM);

	/* allocate urb structures */
	ftd->urb_irq = usb_alloc_urb(0, GFP_KERNEL);
        if(ftd->urb_irq == NULL) return usb_cleanup(ftd, -ENOMEM);

	/* get a handle to the interrupt data pipe */
	pipe = usb_rcvintpipe(udev, endpoint->bEndpointAddress);
	ret = usb_maxpacket(udev, pipe, usb_pipeout(pipe));
	if(ret != USB_PKT_LEN)
		err("strange packet size %d on interrupt pipe, expected %ld",
			ret, USB_PKT_LEN);

	/* initialise irq urb */
	usb_fill_int_urb(ftd->urb_irq, udev, pipe, ftd->irq_data,
			USB_PKT_LEN,
			urb_irq_callback,
			ftd, endpoint->bInterval);
	ftd->urb_irq->transfer_dma = ftd->irq_dma;
	ftd->urb_irq->transfer_flags |= URB_NO_TRANSFER_DMA_MAP;
	ftd->urb_irq->dev = udev;

	/* find out the physical bus location */
	usb_make_path(udev, ftd->phys, sizeof(ftd->phys));
	strlcat(ftd->phys, "/input0", sizeof(ftd->phys));

	/* register settings for the input device */
	input_dev->name = "Freshtel FT-102";
	input_dev->phys = ftd->phys;
	usb_to_input_id(udev, &input_dev->id);
	input_dev->dev.parent = &intf->dev;

	input_set_drvdata(input_dev, ftd);
	input_dev->open = input_open;
	input_dev->close = input_close;

	/* register available key events */
	input_dev->evbit[0] = BIT(EV_KEY);
	set_bit(KEY_PHONE, input_dev->keybit);
	for (i=0;i<256;i++) {
		int k;
		k = map_scancode_to_key(i);
		if(k >= 0) {
			set_bit(k & 0xff, input_dev->keybit);
			if (k >> 8) set_bit(k >> 8, input_dev->keybit);
		}
	}

	usb_set_intfdata(intf, ftd);

	freshtel_init(ftd);

	freshtel_set_ringtone(ftd, default_ringtone, sizeof(default_ringtone));
	
	/* Turn off backlight */
	freshtel_backlight_enable(ftd, 0);
	
	memset(ftd->lcd_canonical, 0x00, LCD_BUFSIZE);
	
	/* Clear screen by forcing an update */
	memset(ftd->lcd_wanted, 0x00, LCD_BUFSIZE);
	memset(ftd->lcd_displayed, 0xff, LCD_BUFSIZE);
	freshtel_lcd_update(ftd);
	
	input_register_device(ftd->idev);

	register_devnode(ftd, intf->minor);
	
	return 0;
}

static struct usb_driver freshtel_driver = {
	.name		= "freshtel",
	.probe		= usb_probe,
	.disconnect	= usb_disconnect,
	.id_table	= usb_table,
};

static int __init freshtel_dev_init(void)
{
	int ret;
	
	err(DRIVER_DESC " " DRIVER_VERSION);
	if((ret = alloc_chrdev_region(&freshtel_dev, 0, FRESHTEL_MAX_DEVICES, "freshtel"))) return ret;	
	err("Using major %d", MAJOR(freshtel_dev));
	if((ret = usb_register(&freshtel_driver))) {
		unregister_chrdev_region(freshtel_dev, FRESHTEL_MAX_DEVICES);
		return ret;
	}
	return ret;
}

static void __exit freshtel_dev_exit(void)
{
	unregister_chrdev_region(freshtel_dev, FRESHTEL_MAX_DEVICES);
	usb_deregister(&freshtel_driver);
}

module_init(freshtel_dev_init);
module_exit(freshtel_dev_exit);

MODULE_DEVICE_TABLE(usb, usb_table);

MODULE_AUTHOR(DRIVER_AUTHOR);
MODULE_DESCRIPTION(DRIVER_DESC);
MODULE_LICENSE("GPL");

